SPDX-FileCopyrightText: 2025 Florian Dubois SPDX-FileCopyrightText: 2025 AlICe laboratory https://alicelab.be
SPDX-License-Identifier: GPL-3.0-or-later
Florian Dubois 23/01/2025 Blender 4.2.2
import bpy
import math
import random
import mathutilsfor obj in bpy.data.objects:
if obj.type != "CAMERA":
bpy.data.objects.remove(obj, do_unlink=True)
bpy.ops.outliner.orphans_purge()cube_size = 100
bpy.ops.mesh.primitive_cube_add(size=cube_size, location=(0, 0, 0))
cube = bpy.context.objectAppliquer un modificateur Wireframe
wireframe_mod = cube.modifiers.new(name="Wireframe", type="WIREFRAME")
wireframe_mod.use_replace = True
wireframe_mod.thickness = 1 # ajuster au besoindef random_unit_vector():
theta = random.uniform(0, 2 * math.pi) # Angle autour de l'axe Z
phi = random.uniform(0, 2 * math.pi) # Angle de l'axe Z vers l'axe Y
x = math.sin(phi) * math.cos(theta)
y = math.sin(phi) * math.sin(theta)
z = math.cos(phi) * math.sin(theta)
if durée == 5:
y = 0
elif durée % 5 == 0:
x = 0
y = 90 * math.pi / 180
else:
diff = durée
while diff > 5:
diff -= 5
y = y * diff
x = x * diff
z = z * diff
return mathutils.Vector((x, y, z)).normalized()Retourne la réflexion d’un vecteur par rapport à une normale.
def reflect_vector(vector, normal): return vector - 2 * vector.dot(normal) * normalCalcule où un rayon (origin + t*direction) intersecte en premier le cube de demi-taille half_size.
def find_intersection(origin, direction, half_size): epsilon = 1e-5
scales = [
(half_size - origin.x) / direction.x if direction.x != 0 else float("inf"),
(-half_size - origin.x) / direction.x if direction.x != 0 else float("inf"),
(half_size - origin.y) / direction.y if direction.y != 0 else float("inf"),
(-half_size - origin.y) / direction.y if direction.y != 0 else float("inf"),
(half_size - origin.z) / direction.z if direction.z != 0 else float("inf"),
(-half_size - origin.z) / direction.z if direction.z != 0 else float("inf"),
]
valid_scales = [s for s in scales if s > epsilon and s != float("inf")]
if valid_scales:
scale = min(valid_scales)
else:Aucun scale strictement positif On peut choisir de ne pas créer de cylindre, ou de mettre scale=0
scale = 0.0
point = origin + direction * scaleClamp pour que le point reste bien dans la surface (à ± half_size)
point.x = max(-half_size, min(half_size, point.x))
point.y = max(-half_size, min(half_size, point.y))
point.z = max(-half_size, min(half_size, point.z))
return pointfraction_min = 0.4 # intensité=1 => 40% du diamètre max
fraction_max = 1.0 # intensité=8 => 100% du diamètre max
steps = 7 # (8 - 1)Calcule la fraction du diamètre max en fonction de l’intensité (1..8). intensité = 1 => fraction_min intensité = 8 => fraction_max
def fraction_for_intensity(i): i_clamp = max(1, min(8, i))
return fraction_min + (i_clamp - 1) * (fraction_max - fraction_min) / stepsvariables = [
{"durée": 2, "répétition": True, "notes": 4, "type": "Sixteenth"}, # 0
{"durée": 2, "répétition": True, "notes": 7, "type": "Sixteenth"}, # 1
{"durée": 5, "répétition": False, "notes": 2, "type": "Long_held"}, # 2
{"durée": 2, "répétition": True, "notes": 3, "type": "Sixteenth"}, # 3
{"durée": 2, "répétition": True, "notes": 1, "type": "Sixteenth"}, # 4
{"durée": 5, "répétition": False, "notes": 1, "type": "Long_held"}, # 5
{"durée": 2, "répétition": True, "notes": 7, "type": "Sixteenth"}, # 6
{"durée": 15, "répétition": False, "notes": 8, "type": "Long_staccato"}, # 7
{"durée": 2, "répétition": True, "notes": 7, "type": "Sixteenth"}, # 8
{"durée": 2, "répétition": True, "notes": 5, "type": "Sixteenth"}, # 9
{"durée": 16, "répétition": False, "notes": 10, "type": "Long_staccato"}, # 10
{"durée": 4, "répétition": True, "notes": 15, "type": "Sixteenth"}, # 11
{"durée": 15, "répétition": False, "notes": 12, "type": "Long_staccato"}, # 12
{"durée": 2, "répétition": True, "notes": 7, "type": "Sixteenth"}, # 13
{"durée": 14, "répétition": False, "notes": 10, "type": "Long_staccato"}, # 14
{"durée": 2, "répétition": True, "notes": 8, "type": "Sixteenth"}, # 15
{"durée": 15, "répétition": False, "notes": 9, "type": "Long_staccato"}, # 16
{"durée": 2, "répétition": True, "notes": 8, "type": "Sixteenth"}, # 17
{"durée": 17, "répétition": False, "notes": 19, "type": "Long_staccato"}, # 18
{"durée": 2, "répétition": True, "notes": 8, "type": "Sixteenth"}, # 19
{"durée": 33, "répétition": False, "notes": 10, "type": "Long_held"}, # 20
{"durée": 2, "répétition": True, "notes": 8, "type": "Sixteenth"}, # 21
{"durée": 36, "répétition": False, "notes": 4, "type": "Long_held"}, # 22
{"durée": 2, "répétition": True, "notes": 8, "type": "Sixteenth"}, # 23
{"durée": 30, "répétition": False, "notes": 17, "type": "Long_staccato"}, # 24
{"durée": 2, "répétition": True, "notes": 8, "type": "Sixteenth"}, # 25
{"durée": 2, "répétition": True, "notes": 8, "type": "Sixteenth"}, # 26
{"durée": 24, "répétition": False, "notes": 10, "type": "Long_held"}, # 27
{"durée": 2, "répétition": True, "notes": 8, "type": "Sixteenth"}, # 28
{"durée": 2, "répétition": True, "notes": 8, "type": "Sixteenth"}, # 29
{"durée": 3, "répétition": True, "notes": 12, "type": "Sixteenth"}, # 30
{"durée": 2, "répétition": True, "notes": 8, "type": "Sixteenth"}, # 31
{"durée": 5, "répétition": False, "notes": 1, "type": "Long_staccato"}, # 32
{"durée": 7, "répétition": False, "notes": 4, "type": "Long_staccato"}, # 33
{"durée": 2, "répétition": True, "notes": 8, "type": "Sixteenth"}, # 34
{"durée": 8, "répétition": False, "notes": 1, "type": "Long_held"}, # 35
{"durée": 2, "répétition": True, "notes": 8, "type": "Sixteenth"}, # 36
{"durée": 6, "répétition": True, "notes": 18, "type": "Sixteenth"}, # 37
{"durée": 4, "répétition": True, "notes": 4, "type": "Sixteenth"}, # 38
{"durée": 3, "répétition": True, "notes": 10, "type": "Sixteenth"}, # 39
{"durée": 3, "répétition": False, "notes": 8, "type": "Sixteenth"}, # 40
{"durée": 5, "répétition": True, "notes": 7, "type": "Sixteenth"}, # 41
{"durée": 20, "répétition": False, "notes": 38, "type": "Sixteenth"}, # 42
{"durée": 2, "répétition": True, "notes": 8, "type": "Sixteenth"}, # 43
{"durée": 5, "répétition": False, "notes": 1, "type": "Long_staccato"}, # 44
{"durée": 2, "répétition": True, "notes": 8, "type": "Sixteenth"}, # 45
{"durée": 6, "répétition": True, "notes": 19, "type": "Sixteenth"}, # 46
{"durée": 2, "répétition": True, "notes": 8, "type": "Sixteenth"}, # 47
{"durée": 2, "répétition": True, "notes": 5, "type": "Sixteenth"}, # 48
{"durée": 30, "répétition": False, "notes": 21, "type": "Long_staccato"}, # 49
{"durée": 2, "répétition": True, "notes": 8, "type": "Sixteenth"}, # 50
{"durée": 3, "répétition": True, "notes": 1, "type": "Sixteenth"}, # 51
{"durée": 2, "répétition": True, "notes": 8, "type": "Sixteenth"}, # 52
{"durée": 5, "répétition": False, "notes": 1, "type": "Long_staccato"}, # 53
{"durée": 5, "répétition": True, "notes": 11, "type": "Sixteenth"}, # 54
{"durée": 2, "répétition": True, "notes": 8, "type": "Sixteenth"}, # 55
{"durée": 5, "répétition": False, "notes": 1, "type": "Long_held"}, # 56
{"durée": 4, "répétition": True, "notes": 11, "type": "Sixteenth"}, # 57
{"durée": 2, "répétition": True, "notes": 1, "type": "Sixteenth"}, # 58
{"durée": 3, "répétition": True, "notes": 8, "type": "Sixteenth"}, # 59
{"durée": 3, "répétition": True, "notes": 8, "type": "Sixteenth"}, # 60
{"durée": 14, "répétition": False, "notes": 3, "type": "Long_held"}, # 61
]Patern_choisi = 11 # indice voulu (depuis la partition)
Patern_ciblé = Patern_choisi - 1
Patern_ciblé_biblio = Patern_ciblé
longueur_interval = 3Filtrer la bibliothèque : extraire 3 éléments autour du pattern
bibliothèque_filtrée = variables[Patern_ciblé_biblio - 1 : Patern_ciblé_biblio + 2]Pattern cible
pattern_cible = variables[Patern_ciblé]
rep = 1 if pattern_cible["répétition"] else 0patern_en_cours = 0
half_size = cube_size / 2
if rep == 0:
for variable in bibliothèque_filtrée:
patern_en_cours += 1
notes = variable["notes"]
durée = variable["durée"]
répétitions = 1
rebonds = 8Ajustements en fonction de patern_en_cours
if variable["répétition"]:
if patern_en_cours == 1:
répétitions = 8
rebonds = 0
if patern_en_cours == 3:
répétitions = 8
rebonds = 8
else:
répétitions = 1
rebonds = 8Calcul du diamètre de base en fonction du type
if variable["type"] == "Sixteenth":
initial_diameter = notes / durée * 3
elif variable["type"] == "Long_staccato":
initial_diameter = notes / durée * 50
elif variable["type"] == "Long_held":
initial_diameter = notes * 20
else:
initial_diameter = notesPréfix selon patern_en_cours
if patern_en_cours == 1:
prefix = "precedent_"
elif patern_en_cours == 2:
prefix = "cible_"
elif patern_en_cours == 3:
prefix = "suivant_"On génère la direction aléatoire 1 seule fois pour chaque pattern
origin = mathutils.Vector((0, 0, 0))
direction = random_unit_vector()
for _ in range(répétitions):Ajuster rebonds
if patern_en_cours == 3:
rebonds -= 1
if patern_en_cours == 1:
rebonds += 1
for rebond_index in range(rebonds):Calcul intensité courante
if patern_en_cours == 1:
intensity_current = rebonds - rebond_index
else:
intensity_current = max(8 - rebond_index, 1)Intensité suivante pour “rebond_index + 1”
if rebond_index < rebonds - 1:
if patern_en_cours == 1:
intensity_next = rebonds - (rebond_index + 1)
else:
intensity_next = max(8 - (rebond_index + 1), 1)
else:Dernier rebond => intensité identique ou au choix
intensity_next = intensity_currentConvertir intensités en diamètres
diameter_current = (
fraction_for_intensity(intensity_current) * initial_diameter
)
diameter_next = (
fraction_for_intensity(intensity_next) * initial_diameter
)Trouver point d’impact
end_point = find_intersection(origin, direction, half_size)
length = (end_point - origin).length
location = (origin + end_point) / 2 bpy.ops.mesh.primitive_cone_add(
vertices=32,
radius1=diameter_current / 2,
radius2=diameter_next / 2,
depth=length,
location=location,
)
cone = bpy.context.object
cone.name = f"{prefix}{cone.name}_rebond_{rebond_index}"Aligner le tronc de cône
cone_vector = end_point - origin
cone.rotation_mode = "QUATERNION"
cone.rotation_quaternion = cone_vector.to_track_quat("Z", "Y")=== (B) CRÉER UNE SPHÈRE À LA JONCTION (end_point) === On lui donne le diamètre du “sommet” du tronçon (diameter_next).
sphere_radius = diameter_next / 2
bpy.ops.mesh.primitive_uv_sphere_add(
radius=sphere_radius, location=end_point
)
sphere = bpy.context.object
sphere.name = f"{prefix}{sphere.name}_rebond_{rebond_index}"(Optionnel) On peut lui donner le même matériau que le cône
if cone.data.materials:
sphere.data.materials.append(cone.data.materials[0]) normal = mathutils.Vector((0, 0, 0))
eps = 1e-4
if abs(end_point.x - half_size) < eps:
normal = mathutils.Vector((1, 0, 0))
elif abs(end_point.x + half_size) < eps:
normal = mathutils.Vector((-1, 0, 0))
elif abs(end_point.y - half_size) < eps:
normal = mathutils.Vector((0, 1, 0))
elif abs(end_point.y + half_size) < eps:
normal = mathutils.Vector((0, -1, 0))
elif abs(end_point.z - half_size) < eps:
normal = mathutils.Vector((0, 0, 1))
elif abs(end_point.z + half_size) < eps:
normal = mathutils.Vector((0, 0, -1))
direction = reflect_vector(direction, normal).normalized()
origin = end_point
if rep == 1:
for variable in bibliothèque_filtrée:
patern_en_cours += 1
notes = variable["notes"]
durée = variable["durée"]
répétitions = 16
rebonds = 8Ajustements en fonction de patern_en_cours
if variable["répétition"]:
if patern_en_cours == 1:
répétitions = 8
rebonds = 0
if patern_en_cours == 3:
répétitions = 8
rebonds = 8
else:
répétitions = 1
rebonds = 8Calcul du diamètre de base en fonction du type
if variable["type"] == "Sixteenth":
initial_diameter = notes / durée * 2
elif variable["type"] == "Long_staccato":
initial_diameter = notes / durée * 100
elif variable["type"] == "Long_held":
initial_diameter = notes * 10
else:
initial_diameter = notesPréfix selon patern_en_cours
if patern_en_cours == 1:
prefix = "precedent_"
elif patern_en_cours == 2:
prefix = "cible_"
elif patern_en_cours == 3:
prefix = "suivant_"On génère la direction aléatoire 1 seule fois pour chaque pattern
origin = mathutils.Vector((0, 0, 0))
direction = random_unit_vector()
for _ in range(répétitions):Ajuster rebonds
if patern_en_cours == 3:
rebonds -= 1
if patern_en_cours == 1:
rebonds += 1
for rebond_index in range(rebonds):Calcul intensité courante
if patern_en_cours == 1:
intensity_current = rebonds - rebond_index
else:
intensity_current = max(8 - rebond_index, 1)Intensité suivante pour “rebond_index + 1”
if rebond_index < rebonds - 1:
if patern_en_cours == 1:
intensity_next = rebonds - (rebond_index + 1)
else:
intensity_next = max(8 - (rebond_index + 1), 1)
else:Dernier rebond => intensité identique ou au choix
intensity_next = intensity_currentConvertir intensités en diamètres
diameter_current = (
fraction_for_intensity(intensity_current) * initial_diameter
)
diameter_next = (
fraction_for_intensity(intensity_next) * initial_diameter
)Trouver point d’impact
end_point = find_intersection(origin, direction, half_size)
length = (end_point - origin).length
location = (origin + end_point) / 2 bpy.ops.mesh.primitive_cone_add(
vertices=32,
radius1=diameter_current / 2,
radius2=diameter_next / 2,
depth=length,
location=location,
)
cone = bpy.context.object
cone.name = f"{prefix}{cone.name}_rebond_{rebond_index}"Aligner le tronc de cône
cone_vector = end_point - origin
cone.rotation_mode = "QUATERNION"
cone.rotation_quaternion = cone_vector.to_track_quat("Z", "Y")=== (B) CRÉER UNE SPHÈRE À LA JONCTION (end_point) === On lui donne le diamètre du “sommet” du tronçon (diameter_next).
sphere_radius = diameter_next / 2
bpy.ops.mesh.primitive_uv_sphere_add(
radius=sphere_radius, location=end_point
)
sphere = bpy.context.object
sphere.name = f"{prefix}{sphere.name}_rebond_{rebond_index}"(Optionnel) On peut lui donner le même matériau que le cône
if cone.data.materials:
sphere.data.materials.append(cone.data.materials[0]) normal = mathutils.Vector((0, 0, 0))
eps = 1e-4
if abs(end_point.x - half_size) < eps:
normal = mathutils.Vector((1, 0, 0))
elif abs(end_point.x + half_size) < eps:
normal = mathutils.Vector((-1, 0, 0))
elif abs(end_point.y - half_size) < eps:
normal = mathutils.Vector((0, 1, 0))
elif abs(end_point.y + half_size) < eps:
normal = mathutils.Vector((0, -1, 0))
elif abs(end_point.z - half_size) < eps:
normal = mathutils.Vector((0, 0, 1))
elif abs(end_point.z + half_size) < eps:
normal = mathutils.Vector((0, 0, -1))
direction = reflect_vector(direction, normal).normalized()
origin = end_pointCrée et assigne un matériau avec une couleur donnée.
def assign_material(obj, color): if obj.type == "MESH":
mat = bpy.data.materials.new(name=f"Material_{color}")
mat.use_nodes = True
nodes = mat.node_tree.nodes
links = mat.node_tree.linksEffacer les noeuds existants
for node in nodes:
nodes.remove(node)Principled BSDF + Material Output
principled_node = nodes.new(type="ShaderNodeBsdfPrincipled")
output_node = nodes.new(type="ShaderNodeOutputMaterial")
links.new(principled_node.outputs["BSDF"], output_node.inputs["Surface"])Couleur de base
principled_node.inputs["Base Color"].default_value = (*color, 1)Assigner le matériau à l’objet
if len(obj.data.materials) == 0:
obj.data.materials.append(mat)
else:
obj.data.materials[0] = mat
color_mapping = {
"cible": (0.8, 0.40, 0.40), # Vert
"precedent": (0.8, 0.60, 0.45), # Bleu
"suivant": (0.95, 0.60, 0.30), # Rouge
"Cube": (0, 0, 0), # Noir
}Appliquer les couleurs aux objets correspondants
bpy.ops.object.select_all(action="DESELECT")
for category, color in color_mapping.items():
bpy.ops.object.select_all(action="DESELECT")
for obj in bpy.data.objects:
if obj.name.startswith(category):
obj.select_set(True)
for obj in bpy.context.selected_objects:
assign_material(obj, color)